﻿using Bettzueche.Dampfross;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Bettzueche.DampfrossContinuum.Experimente
{
    class Experiment1
    {
        public enum Karten
        {
            Map9x10_1,
            Map9x10_2,
            Map9x10_2_3T,
            Map9x10_2_1T4,
            Map9x10_2_1T16,
            Map9x10_2_1T2,
            Map9x10_2_1T1,
            Map14x16_1_1T2,
            Map14x16_1_1T3,
            Map14x16_1_1T9,
            Map14x16_1_1T16,
            Map14x16_1_2T8,
            Map14x16_1_3T9,
            Map14x16_1_C2,
            Map14x16_1_C2b,
            Map14x16_1_C3,
            Map14x16_1_C3b,
            Map14x16_1_C4,
            Map14x16_1_C4b,
            Map14x16_2_C1,
            Map14x16_2_C4,
            Map14x16_2_C4b
        }
        protected static readonly string[] _MapFilenames = { @"Karten\hexMap9x10-01c.bin", @"Karten\hexMap9x10-02c.bin",
            @"Karten\hexMap9x10-02c.bin", @"Karten\hexMap9x10-02c.bin", @"Karten\hexMap9x10-02c.bin", @"Karten\hexMap9x10-02c.bin",
            @"Karten\hexMap9x10-02c.bin", @"Karten\hexMap14x16-01c.bin", @"Karten\hexMap14x16-01c.bin", @"Karten\hexMap14x16-01c.bin",
            @"Karten\hexMap14x16-01c.bin", @"Karten\hexMap14x16-01c.bin", @"Karten\hexMap14x16-01c.bin", @"Karten\hexMap14x16-01c.bin",
            @"Karten\hexMap14x16-01c.bin", @"Karten\hexMap14x16-01c.bin", @"Karten\hexMap14x16-01c.bin", @"Karten\hexMap14x16-01c.bin",
            @"Karten\hexMap14x16-01c.bin", @"Karten\hexMap14x16-02c.bin", @"Karten\hexMap14x16-02c.bin", @"Karten\hexMap14x16-02c.bin"};
        protected static readonly HexKoordinate[] _ZielKordinaten = { new HexKoordinate(3, 8), new HexKoordinate(3, 8), new HexKoordinate(3, 8),
            new HexKoordinate(3, 8), new HexKoordinate(3, 8), new HexKoordinate(3, 8), new HexKoordinate(3, 8), new HexKoordinate(1, 4),
            new HexKoordinate(3, 8), new HexKoordinate(3, 8), new HexKoordinate(3, 8), new HexKoordinate(3, 8), new HexKoordinate(3, 8),
            new HexKoordinate(3, 8), new HexKoordinate(3, 8), new HexKoordinate(3, 8), new HexKoordinate(3, 8), new HexKoordinate(3, 8),
            new HexKoordinate(3, 8), new HexKoordinate(3, 8), new HexKoordinate(3, 8), new HexKoordinate(3, 8), new HexKoordinate(3, 8)};
        protected static readonly string[] _ZidFilenames = { @"Karten\zIDshexMap9x10-01c.csv", @"Karten\zIDshexMap9x10-02c.csv",
            @"Karten\zIDshexMap9x10-02c-3T.csv", @"Karten\zIDshexMap9x10-02c-1T4.csv", @"Karten\zIDshexMap9x10-02c-1T16.csv",
            @"Karten\zIDshexMap9x10-02c-1T2.csv", @"Karten\zIDshexMap9x10-02c-1T1.csv", @"Karten\zIDshexMap14x16-01c-1T2.csv",
            @"Karten\zIDshexMap14x16-01c-1T3.csv", @"Karten\zIDshexMap14x16-01c-1T9.csv", @"Karten\zIDshexMap14x16-01c-1T16.csv",
            @"Karten\zIDshexMap14x16-01c-2T8.csv", @"Karten\zIDshexMap14x16-01c-3T9.csv",
            @"Karten\zIDshexMap14x16-01c-C2.csv", @"Karten\zIDshexMap14x16-01c-C2b.csv", @"Karten\zIDshexMap14x16-01c-C3.csv",
            @"Karten\zIDshexMap14x16-01c-C3b.csv", @"Karten\zIDshexMap14x16-01c-C4.csv", @"Karten\zIDshexMap14x16-01c-C4b.csv",
            @"Karten\zIDshexMap14x16-02c-C1.csv", @"Karten\zIDshexMap14x16-02c-C4.csv", @"Karten\zIDshexMap14x16-02c-C4b.csv"};
        protected static readonly string[] _SollFilenames = { @"Karten\Sollwerte9x10-01c.csv", @"Karten\Sollwerte9x10-02c.csv",
            @"Karten\Sollwerte9x10-02c-3T.csv", @"Karten\Sollwerte9x10-02c-1T4.csv", @"Karten\Sollwerte9x10-02c-1T16.csv",
            @"Karten\Sollwerte9x10-02c-1T2.csv", @"Karten\Sollwerte9x10-02c-1T1.csv", @"Karten\Sollwerte14x16-01-1T2.csv",
            @"Karten\Sollwerte14x16-01-1T3.csv", @"Karten\Sollwerte14x16-01-1T9.csv", @"Karten\Sollwerte14x16-01-1T16.csv",
            @"Karten\Sollwerte14x16-01c-2T8.csv", @"Karten\Sollwerte14x16-01c-3T9.csv", @"Karten\Sollwerte14x16-01c-C2.csv",
            @"Karten\Sollwerte14x16-01c-C2b.csv", @"Karten\Sollwerte14x16-01c-C3.csv", @"Karten\Sollwerte14x16-01c-C3b.csv",
            @"Karten\Sollwerte14x16-01c-C4.csv", @"Karten\Sollwerte14x16-01c-C4b.csv",  @"Karten\Sollwerte14x16-02c-C1.csv", 
            @"Karten\Sollwerte14x16-02c-C4.csv",  @"Karten\Sollwerte14x16-02c-C4b.csv"};
        
        
        /// <summary>
        /// Startkoordinaten, indiziert nach <c>Karten</c>-Typ (enum).
        /// Die Koords haben jeweils alle dieselben Minimalkosten zum Ziel
        /// </summary>
        protected static readonly HexKoordinate[][] _StartKoordinaten;

        static Experiment1() {
            HexKoordinate[] start9x10_01 = new HexKoordinate[]{ // map9x10-01, Kosten=13
                    new HexKoordinate(-1,4), new HexKoordinate(1,4), new HexKoordinate(4,1),
                    new HexKoordinate(7,1), new HexKoordinate(6,3) };
            HexKoordinate[] start9x10_02 = new HexKoordinate[]{ // map9x10-02c, Kosten=11
                    new HexKoordinate(-1,5), new HexKoordinate(-1,7), new HexKoordinate(-2,8),
                    new HexKoordinate(4,1), new HexKoordinate(5,1), new HexKoordinate(7,1) };
            HexKoordinate[] start14x16_01 = new HexKoordinate[]{ // map14x16-01c, Kosten=13
                    new HexKoordinate(0,4), new HexKoordinate(-5,14), new HexKoordinate(4,3),
                    new HexKoordinate(2,14), new HexKoordinate(12,1), new HexKoordinate(10,4),
                    new HexKoordinate(7,9), new HexKoordinate(5,13) };
            HexKoordinate[] start14x16_02 = new HexKoordinate[]{ // map14x16-02c, Kosten=8
                    new HexKoordinate(0,2), new HexKoordinate(-1,4), new HexKoordinate(-5,12),
                    new HexKoordinate(-5,13), new HexKoordinate(-5,14), new HexKoordinate(3,1),
                    new HexKoordinate(11,1), new HexKoordinate(11,2), new HexKoordinate(11,3),
                    new HexKoordinate(6,13), new HexKoordinate(5,14) };
            _StartKoordinaten = new HexKoordinate[][] {
                start9x10_01,  start9x10_02,  start9x10_02,  start9x10_02,  start9x10_02,
                start9x10_02,  start9x10_02,  start14x16_01, start14x16_01, start14x16_01,
                start14x16_01, start14x16_01, start14x16_01, start14x16_01, start14x16_01,
                start14x16_01, start14x16_01, start14x16_01, start14x16_01, start14x16_02,
                start14x16_02, start14x16_02
            };
        }


        #region Fields & Properties

        protected KarteC _umwelt;
        protected AgentC _agent;
        protected int _maxStepsPerEpisode;
        /// <summary>Aktuelle Startkoordinaten, für die gerade geladene Karte. Wird mit LadeTestUmgebung() gesetzt.</summary>
        protected HexKoordinate[] _startKoords;

        /// <summary>
        /// Anzahl Episoden pro Durchlauf. Standard = 500.
        /// </summary>
        /// <value>
        /// The episoden per pass.
        /// </value>
        public int EpisodenPerPass { get; set; }

        /// <summary>
        /// Innerhalb eines Durchlaufs, die Anzahl Episoden, die zu einem Mittelwert zusammengefasst werden.
        /// Standard = 10; Sollte ganzzahlig durch Gesamtzahl Episoden teilbar sein.
        /// </summary>
        /// <value>
        /// The episodes per bundle.
        /// </value>
        public int EpisodesPerBundle { get; set; }

        /// <summary>
        /// Anzahl Durchläufe je Experiment. Standard = 10.
        /// </summary>
        /// <value>
        /// The passes.
        /// </value>
        public int Passes { get; set; }

        #endregion

        /// <summary>
        /// Initializes a new instance of the <see cref="Experiment1"/> class.
        /// Mit Standardwerten: EpisodesPerPass=500, EpisodesPerBundle=10, Passes=10.
        /// </summary>
        public Experiment1() {
            EpisodenPerPass = 500;
            EpisodesPerBundle = 10;
            Passes = 10;
        }

        /// <summary>
        /// Lädt zur spezfizerten Umwelt die Karte und Sollwerte und setzt den (oder einen neuen Agenten) in die Welt.
        /// Hinweis: Beendet automatisch eine Lernepisode durch Agent.Clear();
        /// </summary>
        /// <param name="karte">Typ der Umgebung</param>
        /// <param name="tilingsOrganisation">Unterteilung und Kombination des Landschaftsmerkmals einer Kachel.</param>
        /// <returns>true, wenn Initialisierung erfolgreich; false sonst.</returns>
        public virtual bool InitTestUmgebung(Karten karte, IFeatures<UmgebungC> tilingsOrganisation) {
            // -- Karte laden & Zielkoord festlegen --
            int kartenNr = (int)karte;
            var hexKarte = Helper.LoadMapFromFile<KachelC>(_MapFilenames[kartenNr]);
            if (hexKarte == null) {
                return false;
            }
            KarteC map = new KarteC(hexKarte);
            map.Ziel = _ZielKordinaten[kartenNr];
            map.KachelWertUnterteilung = tilingsOrganisation;
            _umwelt = map;
            if (_agent == null) {
                _agent = new AgentC(_umwelt);
            }
            else
                _agent.Umwelt = _umwelt;

            // -- Debug-States --
            int[][] features = Helper.LadeMerkmale(_ZidFilenames[kartenNr]);
            Dictionary<int,UmgebungC> testStates = new Dictionary<int,UmgebungC>(features.Length + (features.Length >> 1));
            for (int i = 0; i < features.Length; i++) {
                UmgebungC state_i = tilingsOrganisation.GetStateFromFeatures(features[i]);
                testStates[state_i.FeaturedID] = state_i;
            }
            _agent.SetStates(testStates);
            // --- Sollwerte ---
            Dictionary<int, double[]> sollwerte = Helper.GibSollMittelwerte(_SollFilenames[kartenNr]);//Helper.LadeSollwerte(_SollFilenames[kartenNr]);
            if (sollwerte == null)
                return false;
            //if (sollwerte.Count != testStates.Count) {
            //    Debug.WriteLine("Inkonsistente Daten. Anzahl von Zuständen und Sollwert-Arrays stimmen nicht überein.");
            //    return false;
            //}
            _agent.SetSollwerte(sollwerte);

            _maxStepsPerEpisode = 2 * features.Length; // doppelte Landkachelzahl als Schrittlimit pro Epsiode
            _agent.Reset();
            _startKoords = _StartKoordinaten[kartenNr];

            return true;
        }

        /// <summary>
        /// Setzt die Lernparameter des Agenten.
        /// </summary>
        /// <param name="alpha">optional: Lernrate. Standard = 0.3</param>
        /// <param name="lambda">optional: Spurzerfallsfaktor. Standard = 0.7</param>
        /// <param name="gamma">optional: Diskontierungsfaktor. Standard = 1.0</param>
        /// <param name="epsilon">optional: The Explorationsrate. Standard = 0.03</param>
        /// <param name="maxBackView">
        ///     optional: Maximale Anzahl Schritte, die ein Reward zurückreicht. Standard = 42<para>
        ///     Eine zum Spurzerfallsfaktor zusätliche Beschränkung der Lohnberechtigung in die Vergangenheit.</para>
        /// </param>
        /// <returns>true, wenn Initialisierung erfolgreich; false, wenn Agent oder Umwelt uninitialisiert.</returns>
        /// <remarks>
        /// Für alle Parameter wird ein Wert zwischn 0.0 und 1.0 - je inklusive - erwartet. Liegt der angegebene Wert
        /// darüber oder darunter, wird er automatisch auf 1.0 beziehungsweise 0.0 gesetzt.
        /// </remarks>
        public bool SetzeLernparameter(double alpha = 0.3, double lambda = 0.7, double gamma = 1.0, double epsilon = 0.03, int maxBackView = 42) {
            if (_agent == null || _umwelt == null)
                return false;
            // Übernahme der Lernparameter und Beschränkung auf [0;1]
            _agent.Alpha = Math.Min(1.0, Math.Max(0.0, alpha));
            _agent.Lambda = Math.Min(1.0, Math.Max(0.0, lambda));
            _agent.Gamma = Math.Min(1.0, Math.Max(0.0, gamma));
            _agent.Epsilon = Math.Min(1.0, Math.Max(0.0, epsilon));
            _agent.MinBerechtigung = Math.Pow(_agent.Epsilon, maxBackView); //ups! eigentlich: (gamma*lambda)^maxBackView
            return true;
        }

        /// <summary>
        /// Startet ein Experiment und speichert die Ergebnisse als csv im Ordner "Experimente".
        /// </summary>
        /// <param name="episodes">optional: Anzahl Episoden je Durchlauf. Standard = 500.</param>
        /// <param name="bundleCount">optional: Anzahl Episoden, deren Ergebnisse jeweils gemittelt (gebündelt) werden. Standard = 10.<para>
        ///             episodes sollte ganzzahlig durch bundleCount teilbar sein!</para></param>
        /// <param name="passes">optional: Anzahl Durchläufe in diesem Experiment. Standard = 10. Bei jedem Durchlauf wird
        ///             das Agentenwissen gelöscht.</param>
        public virtual void StarteExperiment(int episodes = 500, int bundleCount = 10, int passes = 10) {
            Random random = new Random();
            int bundles = episodes / bundleCount;
            double[][] meanCostsPerPass = new double[passes][];
            double[][] msePerPass = new double[passes][];
            // Je Durchlauf...
            for (int pass = 1; pass <= passes; pass++) {
                _agent.Clear(); // Erfahrung vor jedem Testdurchlauf löschen!

                meanCostsPerPass[pass-1] = new double[bundles];
                msePerPass[pass-1] = new double[bundles];
                int epInBundle = 0;
                int stepCnt = 0;
                double kosten = 0.0;
                double error = 0.0;
                int bundle = 0;
                double[] kostenArr = new double[bundleCount];
                double[] mseArr = new double[bundleCount];
                // Führe Lernepisoden aus:
                for (int episode = 1; episode <= episodes; episode++) {
                    int startNr = random.Next(_startKoords.Length);
                    _umwelt.ReSpawnAgent(_startKoords[startNr]); // Zufallsauswahl für Startfeld
                    _agent.Reset(); // vor jeder Epis. Historie + Reward zurücksetzen
                    stepCnt = _agent.RunEpisode(_maxStepsPerEpisode);
                    kosten = _agent.RewardTotal;
                    kostenArr[epInBundle] = kosten;
                    error = _agent.GetMSE();
                    mseArr[epInBundle] = error;
                    epInBundle++;
                    // nach je 10 Epsidoden, speichere Mittelwerte für reward und error
                    if (epInBundle == bundleCount) {
                        epInBundle = 0;
                        double meanCosts = kostenArr.Average();
                        double meanMSE = mseArr.Average();
                        meanCostsPerPass[pass-1][bundle] = meanCosts;
                        msePerPass[pass-1][bundle] = meanMSE;
                        bundle++;
                    }
                } // Ende des Durchlaufs
            } // Ende Experiment
            // Speichere Daten in csv
            StringBuilder sb = new StringBuilder();
            // --- HEADER ----
            sb.Append("Bundle;");
            for (int p = 1; p <= passes; p++) {
                sb.AppendFormat("Costs(P{0});MSE(P{0});", p);
            }
            sb.Append("Costs;MSE;");
            sb.AppendLine();

            for (int bundle = 1; bundle <= bundles; bundle++) {
                sb.Append(bundle + ";");
                double costSum = .0;
                double mseSum = .0;
                for (int pass = 0; pass < passes; pass++) {
                    sb.Append(meanCostsPerPass[pass][bundle-1] + ";");
                    sb.Append(msePerPass[pass][bundle-1] + ";");
                    costSum += meanCostsPerPass[pass][bundle-1];
                    mseSum += msePerPass[pass][bundle-1];
                }
                double costAvg = costSum / passes;
                double mseAvg = mseSum / passes;
                sb.AppendFormat("{0};{1};", costAvg, mseAvg);
                sb.AppendLine();

            }
            // Lernparameter speichern?
            sb.Append("Epsilon = " + _agent.Epsilon + "  ");
            sb.Append("Alpha = " + _agent.Alpha + "  ");
            sb.Append("Gamma = " + _agent.Gamma + "  ");
            sb.Append("Lambda = " + _agent.Lambda);
            sb.AppendLine();
            sb.Append("Episoden: " + episodes + "  ");
            sb.AppendLine("Ep. je Buendel: " + bundleCount);
            DateTime date = DateTime.Now;
            sb.AppendLine("Datum:" + date.ToShortDateString());
            sb.AppendLine(_umwelt.KachelWertUnterteilung.ToString());
            // Datei (Name?)
            if (!Directory.Exists("Experimente")) {
                Directory.CreateDirectory("Experimente");
            }
            File.WriteAllText(@"Experimente\exp_" + date.ToString("yyMMdd_HH-mm-ss") + ".csv", sb.ToString());
        }

    }
}
